################################################################################
# IMPORTS
################################################################################

import unicodedata
import re

from django.core.urlresolvers import reverse
from django.conf import settings

from django.http import Http404

from django.db import models
from django.utils.translation import ugettext_lazy as _

from taxonomy.utils import *

################################################################################
# CLASS ViewBridge
################################################################################

class ViewBridge(models.Model):
    """
    ViewBridge

    Use this class as a bridge between a translation term and a view. When
    The term is it, instead of rendering the term, the view you specify will
    be called and the response return by the view will be returned directly
    without interference from the taxonomy app.

    You will need to define a regular expression in the same format as your
    URLconf in order to catch any parameters you'd like to be passed to said
    view.
    """

    name = models.CharField(
        _('Name'),
        max_length=80,
        help_text=_('Name of the view bridge')
    )

    path = models.CharField(
        _('path'),
        max_length=256,
        help_text=_('complete view path (that is as if youd import the view')
    )

    params = models.CharField(
        _('Parameters'),
        max_length=1024,
        blank=True,
        null=True,
        help_text=_('regular expression for parameters')
    )

    default_params = models.CharField(
        _('Default Parameters'),
        max_length=1024,
        blank=True,
        null=True,
        help_text=_('default parameters')
    )

    def __unicode__(self):
        return self.name

################################################################################
# CLASS TermTranslation
################################################################################

class TermTranslation(models.Model):
    """
    TermTranslation

    The translation of the name & description of a taxonomy term.
    """

    language = models.CharField(
        _('Language'),
        max_length=5,
        choices=settings.LANGUAGES,
        help_text=_('Language of the translation')
    )

    term = models.ForeignKey(
        'Term',
        verbose_name=_('Term'),
        help_text=_('The term to which this translation applies')
    )

    name = models.CharField(
        _('Name'),
        max_length=80,
        help_text=_('Name translation')
    )

    description = models.CharField(
        _('Description'),
        max_length=2000,
        blank=True,
        null=True,
        help_text=_('Description translation'),
    )

################################################################################
# CLASS Term
################################################################################

class Term(models.Model):
    """
    Term

    A Taxonomy term
    """

    # Term name use getter & setters
    namefield = models.CharField(
        _('Name'),
        max_length=80,
        help_text=_('The name of the taxonomy term')
    )

    def get_name(self):
        """
        Try to find the current language name translation of this term.
        If nothing is found, return the original name.
        """
        lang = get_normalized_language()
        if self.language != lang:
            try:
                tr = TermTranslation.objects.get(term=self.id, language=lang)
                return tr.name
            except:
                return self.namefield
        return self.namefield

    def set_name(self, name):
        """ sets the namefield of this term (eg: this has nothing to do with translation) """
        self.namefield = name

    name = property(get_name, set_name)

    # Term description use getter & setters
    descriptionfield = models.CharField(
        _('Description'),
        max_length=2000,
        blank=True,
        null=True,
        help_text=_('The description of this taxonomy term'),
    )

    def get_description(self):
        """
        Try to find the current language description translation of this term.
        If nothing is found, return the original description.
        """
        lang = get_normalized_language()
        if self.language != lang:
            try:
                tr = TermTranslation.objects.get(term=self.id, language=lang)
                return tr.description
            except:
                return self.descriptionfield
        return self.descriptionfield

    def set_description(self, description):
        """ set the descriptionfield of this term (eg: this has nothing to do with translation) """
        self.descriptionfield = description

    description = property(get_description, set_description)

    weight = models.IntegerField(
        _('Weight'),
        help_text=_('The weight of a term determine its position in a list'),
        default=0
    )

    language = models.CharField(
        _('Language'),
        max_length=5,
        choices=settings.LANGUAGES,
        help_text=_('Language in which this term is expressed')
    )

    parent = models.ForeignKey(
        'Term',
        verbose_name=_('Parent'),
        blank=True,
        null=True,
        related_name='term_fk_parent',
        help_text=_('If this child has a parent term, specify it in this field.')
    )

    use_bridge = models.ForeignKey(
        ViewBridge,
        verbose_name=_('View Bridge'),
        blank=True,
        null=True,
        related_name='viewbridge_fk_bridge',
        help_text=_('If set, skip all normal rendering of this term & return related view response')
    )

    display_children_counts = models.BooleanField(
        _('Display Children Counts'),
        help_text=_('Indicate if children related item count should be rendered along with their name'),
        blank=True,
        null=True,
        default=False
    )

    render_as_instance = models.BooleanField(
        _('Render as Item'),
        help_text=_('If only one item is associated with this term, render this term as if it was the item'),
        blank=True,
        null=True,
        default=False
    )

    render_teasers = models.BooleanField(
        _('Render Teasers'),
        help_text=_('If term has items associated with it, render teasers on page'),
        blank=True,
        null=True,
        default=True
    )

    include_in_sitemap = models.BooleanField(
        _('Include in Sitemap'),
        help_text=_('If true, this term will included in the sitemap.'),
        blank=True,
        null=True,
        default=False
    )

    def get_render_data(self, max_depth=-1):
        """
        retrieve all data necessary to render this taxonomy term and returns it
        in a dictionary format.

        @max_depth: specify the depth at which to go when retrieving children.
            A depth of -1 specify no limit.
            A depth of 0 specify only current node.
            A depth of 1 is used to retrieve only direct children
            and so on...
        """
        data = {
            'term':self,
            'name':self.name,
            'description':self.description,
            'instances':get_related_model_instances(self),
            'children':[],
        }
        if max_depth != 0:
            for child in self.get_children():
                data['children'].append(child.get_render_data(max_depth-1))
        return data

    def get_slug(self):
        """
        Return a slufigied version of term name
        """
        return slugify(self.name)

    def check_slug(self, slug):
        """
        Returns true if this term slugified name match given slug

        @slug: the slug
        """
        return (self.get_slug() == slug)

    def get_breadcrumb(self):
        """
        Return a list containing all the terms that leads from the root to
        this term.
        """
        breadcrumb = [self,]
        parent = self.parent
        while parent:
            breadcrumb.append(parent)
            parent = parent.parent
        breadcrumb.reverse()
        return breadcrumb

    def get_absolute_url(self):
        """
        Returns the absolute path of this node
        """
        return self.get_absolute_path()

    def get_absolute_path(self):
        """
        Returns the absolute path of this node
        """
        breadcrumb = [item.get_slug() for item in self.get_breadcrumb()]
        taxonomy_path = '/'.join(breadcrumb)
        return reverse('taxonomy.views.render', kwargs={'path':taxonomy_path})

    def get_children(self):
        """
        This function is a generator.

        Returns all term children.
        """
        terms = Term.objects.filter(parent=self).order_by('weight')
        for term in terms:
            yield term

    def call_view(self, request, parameter_string):
        """
        if a view (via use_bridge) has been set for this term, call it and return the related
        response object.

        If no view has been set or the view cannot be found, raise an http404.

        @request: the request object
        @parameter_string: The reminder of the path that was used to find this term.
        """
        if self.use_bridge is None:
            raise Http404
        # retrieve the view
        try:
            dot = self.use_bridge.path.rindex('.')
            vpath = self.use_bridge.path[:dot]
            vfunc = self.use_bridge.path[dot+1:]
            view = getattr(__import__(vpath, {}, {}, ['']), vfunc)
        except:
            raise Http404
        expr = self.use_bridge.params
        if len(expr) > 0:
            if expr[-1] != '/':
                expr += '/'
            params = parameter_string if parameter_string else self.use_bridge.default_params
            if params[-1] != '/':
                params += '/'
            m = re.match(expr, params)
            if m is None:
                raise Http404
            kwargs = m.groupdict()
            args = [arg for arg in m.groups() if arg not in kwargs.values()]
            return view(request, *args, **kwargs)
        else:
            return view(request)

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = _('Term')
        verbose_name_plural = _('Terms')
        
################################################################################
# FUNCTION get_roots
################################################################################

def get_roots():
    """
    This function is a generator.

    Returns all term that have no parents (eg: root terms).
    """
    terms = Term.objects.filter(parent=None).order_by('weight')
    for term in terms:
        yield term

################################################################################
# FUNCTION find_term_by_path
################################################################################

def find_term_by_path(path):
    """
    Try to find the term that can be accessed via the given path, if it's
    not found, return None

    @path: the request path
    """
    if not path:
        return None
    p = path if path[-1] != '/' else path[:-1]
    assert(path[-1] != '/')
    path_item = path.split('/')
    terms = [term for term in get_roots()]
    while len(path_item) > 0:
        slug = path_item.pop(0)
        for term in terms:
            if term.check_slug(slug):
                if len(path_item) > 0:
                    terms = [child for child in term.get_children()]
                    break
                else:
                    return term
    return None

################################################################################
# FUNCTION find_term_and_parameters
################################################################################

def find_term_and_parameters(path):
    """
    Try to find the term that can be accessed via the given path

    start from the root and stop when no term can be found, return the rest
    of the path as the parameter string.

    Always return a tuple of the form (term, path) Any of which can be None
    depending on the lookup results

    @path: the request path
    """
    if not path:
        return (None, None)
    path_item = path.split('/')
    cpath = ''
    while len(path_item) > 0:
        temp_path = cpath + '/' + path_item[0]
        term = find_term_by_path(temp_path)
        if term:
            cpath += path_item.pop(0)
        else:
            break
    term = find_term_by_path(cpath)
    if not term:
        return (None,None)
    if len(path_item) > 0:
        return (term, '/'.join(path_item))
    else:
        return (term, None)

################################################################################
# FUNCTION find_model_instance_by_path
################################################################################

def find_model_instance_by_path(path):
    """
    Try to find a model instance with the given path.

    Use all the path item but the last to retrieve terms, and use the last
    path item as the instance title slug.

    term-a/term-b/instance-a

    will return a positive results IF an instance with a slugified title of instance-a
    can be found via a relation from a term with name slug 'term-b' and that this term
    has a parent with name slug 'term-a'

    Always return a tuple of the form (term, instance_data) Any of which can be None
    depending on the lookup results

    instance_data is a dictionnary as returned by utils.get_model_instance_data

    @path: the path
    """
    if not path:
        return (None, None)
    p = path if path[-1] != '/' else path[:-1]
    assert(path[-1] != '/')
    path_item = path.split('/')
    path = '/'.join(path_item[:-1])
    slug = path_item[-1]
    term = find_term_by_path(path)
    if not term:
        return (None, None)
    instances = get_related_model_instances(term)
    for instance_data in instances:
        if slugify(instance_data['data']['title']) == slug:
            return (term, instance_data)
    return (None, None)

